package server;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.sql.SQLException;
import java.util.TreeMap;

import dao.ContatoDao;

import log.GeradorDeLog;
import model.Configuracao;
import model.Contato;
import model.Enviavel;
import model.MensagemDeAutenticacao;
import model.MensagemDeListaDeContatos;

public final class ServidorDeContatos {

	private TreeMap<Contato, String> contatosOnline;
	private ServerSocket socketDoCliente;
	private GeradorDeLog log;

	public static void main(String[] args) {
		Thread thread = new Thread(new Runnable() {

			@Override
			public void run() {
				// TODO Auto-generated method stub
				@SuppressWarnings("unused")
				ServidorDeContatos servidorDeContato = new ServidorDeContatos();
			}
		});
		// inicia a thread
		thread.start();
	}

	public ServidorDeContatos() {
		log = new GeradorDeLog();
		contatosOnline = new TreeMap<Contato, String>();

		log.escreve("Servidor Iniciado");

		try {
			recebeMensagensDeAutenticacao();
		} catch (Exception e) {
			System.out.println(e);
		}

	}

	private void autentica(MensagemDeAutenticacao autenticacao) {
		if (autenticacao.isTipo() == MensagemDeAutenticacao.LOGIN)
			login(autenticacao);
		else
			logout(autenticacao);
	}

	private void login(MensagemDeAutenticacao autenticacao) {
		log.escreve("Login requisitado");
		log.escreve("Usuario " + autenticacao.getUsuario().getLogin()
				+ " requisita autenticacao");

		try {
			// Cria objeto para requisicao com o database
			ContatoDao dao = new ContatoDao();
			// tenta pegar contato com login e senha passados
			Contato contato = dao.getContato(autenticacao.getUsuario()
					.getLogin(), autenticacao.getSenha());
			if (contatosOnline.get(contato) != null) {
				// contato ja esta logado
				autenticacao.setValidacao(false);
				log.escreve("Este usuario ja esta online");
				envia(autenticacao);
				log.escreve("Autenticacao reenviada ao cliente");
			} else {
				// autenticacao valida
				// caso contato seja null gerará uma excecao
				log.escreve("Login valido: Usuario " + contato.getLogin()
						+ " logado");
				// prepara a classe de autenticacao para reenvio
				autenticacao.setValidacao(true);
				autenticacao.setUsuario(contato);
				// poe o contato na lista de usuario on-line
				contatosOnline.put(contato, autenticacao.getIpDestino());
				// reenvia a resposta ao cliente
				envia(autenticacao);
				log.escreve("Autenticacao reenviada ao cliente");
				// envia os contatos logados para o usuario que acaba de entrar
				enviaContatosParaUsuarioLogado(contato);
				// comunica a todos os usuarios logados quem acaba de se logar
				enviaContatoParaTodos(contato, true);
			}
		} catch (SQLException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NullPointerException e) {
			// login ou senha invalida, contato null
			autenticacao.setValidacao(false);
			log.escreve("Login ou Senha invalido");
			log.escreve("Autenticacao reenviada ao cliente");
		}

		/*
		 * Estava dando nullPointer Exception, pois se o usuário não for
		 * autenticado ele não recebe nenhuma lista de contatos // envia os
		 * contatos logados para o usuario que acaba de entrar
		 * enviaContatosParaUsuarioLogado(contato); // comunica a todos os
		 * usuarios logados quem acaba de se logar
		 * enviaContatoParaTodos(contato, true);
		 */}

	private void logout(MensagemDeAutenticacao autenticacao) {
		log.escreve("Logout requisitado");
		contatosOnline.remove(autenticacao.getUsuario());
		log.escreve(autenticacao.getUsuario().getLogin() + " se deslogou");
		// comunica a todos os usuarios logados quem acaba de se logar
		enviaContatoParaTodos(autenticacao.getUsuario(), false);

		// nao necessita enviar ao cliente, uma vez que o cliente nao necessita
		// ser autorizado a deslogar. este controle é para o servidor nao vir a
		// tentar fazer um logout de alguem que nao está logado

	}

	private void enviaContatosParaUsuarioLogado(Contato usuario) {
		MensagemDeListaDeContatos mensagemDeListaDeContatos = null;
		try {
			// instancia a lista de contatos passando todos os contatos que
			// estao online
			mensagemDeListaDeContatos = new MensagemDeListaDeContatos(
					contatosOnline);
			log.escreve("Lista de contatos criada");

		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// seta o ip do destino da mensagem
		mensagemDeListaDeContatos.setIpDestino(contatosOnline.get(usuario));
		// seta o ip do remetente, o servidor
		mensagemDeListaDeContatos.setIpRemetente(Configuracao.SERVIDOR_IP);
		log.escreve("Ip de destino e remetente tracado");
		try {
			// envia a lista de contatos de volta para o cliente
			envia(mensagemDeListaDeContatos);
			log.escreve("Lista de contatos online enviada para "
					+ usuario.getLogin());
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	@SuppressWarnings("unchecked")
	private void enviaContatoParaTodos(Contato usuario, boolean tipo) {
		MensagemDeListaDeContatos listaDeContatos = null;
		// crio um novo mapa interno que recebe todos os contatos on-line
		TreeMap<Contato, String> contatosParaNotificar = null;
		try {
			contatosParaNotificar = (TreeMap<Contato, String>) contatosOnline
					.clone();
		} catch (ClassCastException e) {
			e.printStackTrace();
		}

		// remove o contato que logou do mapa interno, pois este nao recebera
		// notificacao de que ele proprio logou/deslogou
		contatosParaNotificar.remove(usuario);
		try {
			// cria nova lista de contatos apenas com o contato que se
			// logou/deslogou
			listaDeContatos = new MensagemDeListaDeContatos(usuario,
					contatosOnline.get(usuario));
			listaDeContatos.setTipo(tipo);
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// operacao feita para cada usuario on line, exeto o que acaba de
		// logar/deslogar
		for (String ip : contatosParaNotificar.values()) {
			// seta o ip de destino
			listaDeContatos.setIpDestino(ip);
			try {
				// envia a lista de contatos
				envia(listaDeContatos);
				log.escreve(ip + " foi comunicado da alteracao de estado de "
						+ usuario.getLogin());
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	public void envia(Enviavel enviavel) throws IOException {
		Socket socketParaCliente = new Socket(enviavel.getIpDestino(),
				Configuracao.PORTA_COMUNICACAO);
		ObjectOutputStream fluxoParaCliente = new ObjectOutputStream(
				socketParaCliente.getOutputStream());
		log.escreve("Conexao estabelecida com o cliente");
		fluxoParaCliente.writeObject(enviavel);
		fluxoParaCliente.close();
		socketParaCliente.close();

	}

	private void recebeMensagensDeAutenticacao() {

		// abre o socket de hand-shake
		try {
			socketDoCliente = new ServerSocket(Configuracao.PORTA_COMUNICACAO);
			log.escreve("Socket de espera aberto");
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}

		Socket conexaoDoCliente = null;
		ObjectInputStream fluxoDoCliente = null;

		// loop infinito
		log.escreve("Aguardando requisicao");
		log.fechaLog();
		while (true) {
			log = new GeradorDeLog();
			try {
				// aceita a aconexao
				conexaoDoCliente = socketDoCliente.accept();
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
			log.escreve("Conexao aceita para "
					+ conexaoDoCliente.getInetAddress().getHostAddress());

			try {
				// instancia o fluxo
				fluxoDoCliente = new ObjectInputStream(conexaoDoCliente
						.getInputStream());
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
			log.escreve("Fluxo Criado");
			// o servidor só deve receber autenticacao, nao lista de contatos

			MensagemDeAutenticacao mensagemDeAutenticacao = null;
			try {
				// tenta pegar o objeto autenticação dentro do fluxo
				// autenticacao = (Autenticacao) fluxoDoCliente.readObject();
				mensagemDeAutenticacao = (MensagemDeAutenticacao) fluxoDoCliente
						.readObject();
				log.escreve("Objeto de autenticacao desserializado");
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (ClassNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (ClassCastException e) {
				log.escreve("Mensagem rejeitada");
				e.printStackTrace();
			}

			// adiciona o ip do remetente, ou seja, o proprio servidor
			mensagemDeAutenticacao.setIpRemetente(Configuracao.SERVIDOR_IP);
			// adiciona o IP de destino(cliente) à mensagem
			mensagemDeAutenticacao.setIpDestino(conexaoDoCliente
					.getInetAddress().getHostAddress());
			log
					.escreve("Ip do destino e remetente adicionado ao objeto de autenticacao");
			// tenta autenticar
			autentica(mensagemDeAutenticacao);
			// fecha os sockets e fluxos
			try {
				fluxoDoCliente.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			try {
				conexaoDoCliente.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

			log.fechaLog();
		}

	}

}
